package com.fbs.wowo.manage.service.imp;

import com.fbs.wowo.exception.*;
import com.fbs.wowo.manage.entity.Account;
import com.fbs.wowo.manage.entity.User;
import com.fbs.wowo.manage.entity.UserLogin;
import com.fbs.wowo.manage.mapper.AccountMapper;
import com.fbs.wowo.manage.mapper.UserLoginMapper;
import com.fbs.wowo.manage.mapper.UserMapper;
import com.fbs.wowo.manage.service.LoginService;
import com.fbs.wowo.manage.service.VerifyCodeService;
import com.fbs.wowo.manage.vo.TokenVo;
import com.fbs.wowo.manage.vo.UserInfoVo;
import com.fbs.wowo.manage.vo.UserLoginVoPC;
import com.fbs.wowo.security.token.NothingToDoToken;
import com.fbs.wowo.util.common.Constants;
import com.fbs.wowo.util.common.Generator;
import com.fbs.wowo.util.common.Validator;
import com.fbs.wowo.util.eos.EOSService;
import com.fbs.wowo.util.rongcloud.RCUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.crypto.RandomNumberGenerator;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.util.Date;
import java.util.Map;
import java.util.UUID;

/*******************************
 *
 * 登录相关
 * 使用id做princpal
 *
 * TODO 将登录user_login里的电话和密码抽出来。
 *
 *******************************/
@Service("loginService")
@Transactional
public class LoginServiceImp implements LoginService {
    // 登录方式
    private static final Byte LOGIN_TEL = 1;    // 手机登录
    private static final Byte LOGIN_PC = 2;     // 电脑登录
    private static final Byte LOGIN_WC = 3;     // 微信登录
    private static final Byte LOGIN_WB = 4;     // 微博登录
    private static final Byte LOGIN_QQ = 5;     // QQ登录
    private static final Byte LOGIN_FB = 6;     // facebook登录

    private static final Byte REG = 1;
    private static final Byte LOGIN = 0;

    /**
     *
     * 注册流程：
     *
     * 1. 根据发送手机号生成验证码 并发送短信到手机 判断是新用户还是老用户，返回结果
     * 2. 手机号和验证码一起发送后台，判断验证码是否输入正确，若不正确返回错误，若正确，
     *      2.1. 老用户登录判断
     *      2.2. 新用户注册判断
     *      * 2.2.1 判断邀请码是否有效
     *          * 2.2.1.1 有效 临时对象转换为永久对象
     *          * 2.2.1.2 无效 依然返回
     *  （或将验证码返回，前端判断验证码输入是否正确？）
     *
     * 3. 新用户补全信息
     */
    /**
     * 1.获取到手机号 根据手机号生成验证码 并发送短信到手机
     * * 1.1 老用户
     * * 1.2 新用户
     * 创建临时登录注册对象
     *
     * 如果再次发送验证码 则不插入数据库，只重新发送验证码，并且修改数据库的验证码
     * @param tel
     * @return
     */
    @Override
    public UserLogin sendVerifyCodeLogin(String tel) throws GivenNullParamException {
        Validator.verifyStr(tel);

        UserLogin record = new UserLogin();
        record.setTel(tel);
        // 按手机号码查询User，判断该用户是新用户还是老用户
        User u = getByTel(record.getTel());
        record.setIsReg(u == null ? REG : LOGIN);
        // 生成验证码
        record.setVerifyCode(Generator.generateVerifyCode(record.getTel()));

        record.setCreateTime(new Date());
        record.setLoginType(LOGIN_TEL);

        record.setStatus(VerifyCodeServiceImp.Status_Valid);
        // 生成uuid
        UserLogin lv = userLoginMapper.findByTel(tel);
        if (lv != null) {
            record.setId(lv.getId());
            modifyUserLogin(record);
        } else {
            record.setId(Generator.generateId());
            insertUserLogin(record);
        }
        return record;
    }

    /**
     * 新用户注册
     * 2. 手机号和验证码一起发送后台，判断验证码是否输入正确，若不正确返回错误，
     * （或将验证码返回，前端判断验证码输入是否正确？）
     * * 2.1 判断邀请码是否有效
     * * 2.1.1 有效 临时对象转换为永久对象
     * * 2.1.2 无效 依然返回
     *
     *  TODO {"code":20005,"msg":"Name 参数为必传项"}
     *  要么验证验证码步骤内创建融云账户，并随机赋予一个融云名字，
     *  要么当前步骤不注册融云账户，等提交信息时注册
     * @param record
     * @return
     */
    @Override
    public UserLogin regMob(UserLogin record, String ip) throws GivenNullParamException, InputVerifyFailedException, NoSuchDataException {
        if (record == null || !StringUtils.hasText(record.getVerifyCode()))
            throw new GivenNullParamException();
        // 按手机号码查询LoginVo，判断验证码输入是否正确
        UserLogin userLogin = verifyCodeService.verifyCode(record.getTel(), record.getVerifyCode());
        // 如果是老用户则直接返回
        // 您的号码已注册
        User u = getByTel(record.getTel());
        if (u != null) throw new InputVerifyFailedException(ErrorCode.ERR_TEL_EXIST);

        // 判断邀请码是否有效

        // 生成新用户
        User user = new User();
        user.setId(Generator.generateId());
        String randomCode = Generator.randomCode();
        // 生成userId
        user.setUserId("xi" + randomCode);
        // 生成自己的邀请码
        user.setInvitationCode(randomCode);
        user.setCrewNum(0);
        // 融云：注册用户
        if (!StringUtils.hasText(record.getName()))
            user.setName(UUID.randomUUID().toString().substring(3, 20));    // TODO 临时赋予user name
        user.setPhoto(Constants.DefaultPortrait);       // TODO 临时赋予user photo 需要默认头像
        // 反复创建的视为同一用户
        user.setRcToken(RCUtils.createRCToken(user.getUserId(), user.getName(), user.getPhoto()));
        user.setStatus((byte) 1);
        user.setTel(record.getTel());
        user.setCreateTime(new Date());
        user.setAge(record.getAge() == null ? 0 : record.getAge());
        user.setSex(record.getSex() == null ? 1 : record.getSex());

        // 更新手机盐值
        RandomNumberGenerator rng = new SecureRandomNumberGenerator();
        ByteSource telSalt = rng.nextBytes();
        // 将盐插入数据库
        user.setTelSalt(telSalt.toBase64());
        // 根据tel和手机盐值生成token
        String hashedPasswordBase64 = new Sha256Hash(user.getTel(), telSalt, 1024).toBase64();
        user.setToken(hashedPasswordBase64);

        saveUser(user);
        initAccount(new BigDecimal(0), user.getUserId());

        // 注册完直接登录
        // TODO 从数据库拿数据
        User udb = userMapper.findByUserId(user.getUserId());
        configUserLogin(ip, userLogin, udb);

        return userLogin;
    }

    /**
     * 老用户登录 LoginVo
     * @param record
     * @return
     *
     */
    @Override
    public UserLogin loginMob(UserLogin record, String ip) throws GivenNullParamException, InputVerifyFailedException, NoSuchDataException {
        Validator.verifyNon(record);
        // TODO 傻吊苹果test账号
        if ("15372049520".equals(record.getTel())) {
            User user = getByTel(record.getTel());
            record.setId(user.getId());
            record.setLoginType((byte) 1);
            record.setIsReg((byte) 0);
            configUserLogin(ip, record, user);
            return record;
        }
        // 按手机号码查询LoginVo，判断验证码输入是否正确
        UserLogin userLogin = verifyCodeService.verifyCode(record.getTel(), record.getVerifyCode());

        String tel = userLogin.getTel();
        // 老用户判断
        User user = getByTel(tel);
        if (user == null) throw new NoSuchDataException(ErrorCode.ERR_USER_NOT_EXIST);

        configUserLogin(ip, userLogin, user);

        return userLogin;
    }

    @Override
    public User findByTelAndValid(String tel) {
        return userMapper.findByTelAndStatus(tel);
    }

    /**
     * TODO 电脑端登录
     */
    // 电脑端注册
    @Override
    public TokenVo regPC(UserLoginVoPC record, String ip) throws GivenNullParamException, InputVerifyFailedException, NoSuchDataException {
        if (record == null || !StringUtils.hasText(record.getTel()) || !StringUtils.hasText(record.getPassword()))
            throw new GivenNullParamException();

        // 按手机号码查询LoginVo，判断验证码输入是否正确
        verifyCodeService.verifyCode(record.getTel(), record.getVerifyCode());
        // 如果是老用户则直接返回
        // 您的号码已注册
        User u = getByTel(record.getTel());
        if (u != null) throw new InputVerifyFailedException(ErrorCode.ERR_TEL_EXIST);

        // 生成新用户
        User user = new User();
        user.setId(Generator.generateId());

        String randomCode = Generator.randomCode();
        // 生成userId
        user.setUserId("xi" + randomCode);
        // 生成自己的邀请码
        user.setInvitationCode(randomCode);
        user.setCrewNum(0);
        // 融云：注册用户
        if (!StringUtils.hasText(record.getName()))
            user.setName(UUID.randomUUID().toString().substring(3, 20));    // TODO 临时赋予user name
        else user.setName(record.getName());
        if (!StringUtils.hasText(record.getPhoto())) {
            user.setPhoto(Constants.DefaultPortrait);       // TODO 临时赋予user photo 需要默认头像
        } else user.setPhoto(record.getPhoto());

        // 反复创建的视为同一用户
        user.setRcToken(RCUtils.createRCToken(user.getUserId(), user.getName(), user.getPhoto()));
        user.setStatus((byte) 1);
        user.setTel(record.getTel());
        user.setCreateTime(new Date());
        user.setAge((byte)0);
        user.setSex((byte)1);

        // 更新手机盐值
        RandomNumberGenerator rng = new SecureRandomNumberGenerator();
        ByteSource salt = rng.nextBytes();
        // 将盐插入数据库
        user.setSalt(salt.toBase64());
        // 根据tel和手机盐值生成token
        String password = new Sha256Hash(record.getPassword(), salt, 1024).toBase64();
        user.setPassword(password);

        saveUser(user);
        initAccount(new BigDecimal(0), user.getUserId());

        // 注册完直接登录
        // 调用电脑端登录subject.login
        return pcLoginToken(ip, record);
    }

    // 电脑端登录
    @Override
    public TokenVo loginPC(UserLoginVoPC record, String ip) throws GivenNullParamException, InputVerifyFailedException, NoSuchDataException {
        if (record == null || !StringUtils.hasText(record.getPassword()))
            throw new GivenNullParamException();
        // 如果是老用户则直接返回
        // 您的号码已注册
        User u = getByTel(record.getTel());
        if (u == null) throw new InputVerifyFailedException(ErrorCode.ERR_USER_NOT_EXIST);

        // 调用电脑端登录subject.login
        return pcLoginToken(ip, record);
    }

    // 忘记密码
    @Override
    public void forget(String tel, String verifyCode, String newPassword) throws NoSuchDataException {
        // tel verifyCode newPassword 判空
        verifyCodeService.verifyCode(tel, verifyCode);
        User udb = userMapper.findByTelAndStatus(tel);
        if (udb == null) throw new NoSuchDataException(ErrorCode.ERR_TEL_VERIFY_CODE);

        User user = new User();
        user.setId(udb.getId());
        // 更新手机盐值
        RandomNumberGenerator rng = new SecureRandomNumberGenerator();
        ByteSource salt = rng.nextBytes();
        // 将盐插入数据库
        user.setSalt(salt.toBase64());
        // 根据密码 盐值生成新加密密码
        String newPasswordEncode = new Sha256Hash(newPassword, salt, 1024).toBase64();
        user.setPassword(newPasswordEncode);
        updateUser(user);
    }

    /*******************************
     *
     * 私有方法
     *
     *******************************/
    /**
     * 填充登录对象
     *
     * @param ip        ip
     * @param userLogin 必须是从数据库获取到的对象
     * @param user      必须是从数据库获取到的对象
     */
    private UserLogin configUserLogin(String ip, UserLogin userLogin, User user) {
        // TODO 各种登录设置
        String token = telLogin(ip, user);
        // userLogin对象填充userId, token等属性
        // TODO 融云等各种数据填充
        userLogin.setName(user.getName());
        userLogin.setUserId(user.getUserId());
        userLogin.setToken(token);
        userLogin.setAge(user.getAge());
        userLogin.setInvitationCode(user.getInvitationCode());
        userLogin.setPhoto(user.getPhoto());
        userLogin.setRcToken(user.getRcToken());
        userLogin.setSex(user.getSex());
        userLogin.setStatus((byte) 1);
        return userLogin;
    }

    /**
     * 按电话号码查找用户
     */
    private User getByTel(String tel) {
        return userMapper.findByTel(tel);
    }

    /**
     * 更新用户
     */
    private void updateUser(User user) {
        userMapper.updateByPrimaryKeySelective(user);
    }

    /**
     * 手机登录
     *
     * TODO 修改：将登录状态放入一个特殊的token，在filter判断，如果手机，直接登录，如果电脑，账号密码判断后登录，
     * @param ip
     * @param user
     * @return 手机token
     * @throws AuthorizationException
     */
    private String telLogin(String ip, User user) throws AuthorizationException {
        Subject currentUser = SecurityUtils.getSubject();

        String key = new Sha256Hash(user.getUserId()).toBase64();

        if (!currentUser.isAuthenticated()) {
            User udb = findUserByUserId(user.getUserId());
            if (udb == null) throw new AuthorizationException("账户不存在");
            // 手机登录能获取到验证码即判断登录成功，无需再次加密参数进行登录确认
            NothingToDoToken nothingToDoToken = new NothingToDoToken();
            nothingToDoToken.setUserId(udb.getUserId());
            nothingToDoToken.setPassword(udb.getUserId());
            currentUser.login(nothingToDoToken);

            // 更新手机盐值
            RandomNumberGenerator rng = new SecureRandomNumberGenerator();
            ByteSource telSalt = rng.nextBytes();
            // 将盐插入数据库
            udb.setTelSalt(telSalt.toBase64());
            // TODO 根据tel和手机盐值生成token
            String hashedPasswordBase64 = new Sha256Hash(udb.getTel(), telSalt, 1024).toBase64();
            udb.setToken(hashedPasswordBase64);
            // TODO 更新登录时间和ip
            udb.setLastLoginTime(new Date());
            udb.setIp(ip);
            updateUser(udb);
            // 登录成功
        }
        return key;
    }

    /**
     * 电脑登录 依然使用token维持登录状态
     */
    private TokenVo pcLoginToken(String ip, UserLoginVoPC user) throws AuthorizationException {
        Subject currentUser = SecurityUtils.getSubject();
        // 根据手机号获取用户
        User udb = findByTelAndValid(user.getTel());
        if (udb == null) throw new AuthorizationException("账户不存在");
        TokenVo vo = new TokenVo();
        vo.setUuQieKeNaoId(udb.getUserId());
        vo.setKey(new Sha256Hash(udb.getUserId()).toBase64());

        if (!currentUser.isAuthenticated()) {
            UsernamePasswordToken token = new UsernamePasswordToken(udb.getUserId(), user.getPassword());
            try {
                currentUser.login(token);
                // 更新手机盐值
                RandomNumberGenerator rng = new SecureRandomNumberGenerator();
                ByteSource salt = rng.nextBytes();
                // 将盐插入数据库
                udb.setSalt(salt.toBase64());
                // 根据密码 盐值生成新加密密码
                String newPassword = new Sha256Hash(user.getPassword(), salt, 1024).toBase64();
                udb.setPassword(newPassword);
                // 更新登录时间和ip
                udb.setLastLoginTime(new Date());
                udb.setIp(ip);
                updateUser(udb);
            } catch (UnknownAccountException uae) {
                throw new AuthorizationException("There is no user with username of " + token.getPrincipal());
            } catch (IncorrectCredentialsException ice) {
                throw new AuthorizationException("Password for account " + token.getPrincipal() + " was incorrect!");
            } catch (LockedAccountException lae) {
                throw new AuthorizationException("The account for username " + token.getPrincipal() + " is locked.  " +
                        "Please contact your administrator to unlock it.");
            } catch (AuthenticationException ae) {
                // ... catch more exceptions here (maybe custom ones specific to your application?
                //unexpected condition?  error?
                ae.printStackTrace();
                throw new AuthorizationException("未知权限校验错误");
            }
        }

        return vo;
    }

    /**
     * 电脑登录
     * @Deprecated 使用session维护登录状态，已过时，替代方法：
     */
    @Deprecated
    private void pcLogin(String ip, UserLoginVoPC user) throws AuthorizationException {
        Subject currentUser = SecurityUtils.getSubject();

        if (!currentUser.isAuthenticated()) {
            // TODO
            // 根据手机号获取用户
            User udb = findByTelAndValid(user.getTel());
            if (udb == null) throw new AuthorizationException("账户不存在");

            UsernamePasswordToken token = new UsernamePasswordToken(udb.getUserId(), user.getPassword(), udb.getSalt());
            token.setRememberMe(true);

            try {
                currentUser.login(token);

                // 更新手机盐值
                RandomNumberGenerator rng = new SecureRandomNumberGenerator();
                ByteSource salt = rng.nextBytes();
                // 将盐插入数据库
                udb.setSalt(salt.toBase64());
                // 根据密码 盐值生成新加密密码
                String newPassword = new Sha256Hash(user.getPassword(), salt, 1024).toBase64();
                udb.setPassword(newPassword);
                // 更新登录时间和ip
                udb.setLastLoginTime(new Date());
                udb.setIp(ip);
                updateUser(udb);

                currentUser.getSession().setAttribute(Constants.PARAM_USER, new UserInfoVo(udb.getUserId(), user.getName(), user.getTel()));
            } catch (UnknownAccountException uae) {
                throw new AuthorizationException("There is no user with username of " + token.getPrincipal());
            } catch (IncorrectCredentialsException ice) {
                throw new AuthorizationException("Password for account " + token.getPrincipal() + " was incorrect!");
            } catch (LockedAccountException lae) {
                throw new AuthorizationException("The account for username " + token.getPrincipal() + " is locked.  " +
                        "Please contact your administrator to unlock it.");
            } catch (AuthenticationException ae) {
                // ... catch more exceptions here (maybe custom ones specific to your application?
                //unexpected condition?  error?
                ae.printStackTrace();
                throw new AuthorizationException("未知权限校验错误");
            }
        }
    }

    private int saveUser(User user) {
        return userMapper.insertSelective(user);
    }

    private User findUserByUserId(String userId) {
        return userMapper.findByUserId(userId);
    }

    /**
     * TODO 初始化账户
     */
    private void initAccount(BigDecimal initBalance, String userId) {
        Account account = new Account();
        account.setId(Generator.generateId());
        account.setLockBalance(new BigDecimal(0));
        account.setBalance(initBalance);
        account.setUserId(userId);
        account.setAddress("");
        account.setPassword("");
        account.setPassword("");
        Map<String, String> key = eosService.EOSKey();
        String privateKey = key.get("privateKey");
        account.setPrivateKey(privateKey);
        String publicKey = key.get("publicKey");
        account.setPublicKey(publicKey);

        eosService.newaccount(userId, publicKey, Constants.getEosCreatorPriKey());
        saveAccount(account);
    }

    private Account findAccountByUserId(String userId) {
        return accountMapper.findByUserId(userId);
    }

    private void saveAccount(Account account) throws DatabaseException {
        try {
            accountMapper.insertSelective(account);
        } catch (Exception e) {
            throw new DatabaseException(e.getMessage());
        }
    }

    private void modifyUserLogin(UserLogin record) {
        userLoginMapper.updateByPrimaryKeySelective(record);
    }

    private void insertUserLogin(UserLogin record) {
        userLoginMapper.insertSelective(record);
    }

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private UserLoginMapper userLoginMapper;
    @Autowired
    private AccountMapper accountMapper;
    @Autowired
    private VerifyCodeService verifyCodeService;
    @Autowired
    private EOSService eosService;
}
